import {
  XcodeProject,
  type PBXFileReference,
  type PBXGroup,
  type PBXShellScriptBuildPhase,
} from '@bacons/xcode';
import { build as buildXcodeProject } from '@bacons/xcode/json';
import { IOSConfig, type ModPlatform } from 'expo/config-plugins';
import fs from 'fs/promises';
import path from 'path';

import type { WorkingDirectories } from './workingDirectories';

interface Params {
  projectRoot: string;
  workingDirectories: WorkingDirectories;
  platform: ModPlatform;
  backup: boolean;
}

type BackupFileMappings = Record<string, string>;

/**
 * Normalize the native projects to minimize the diff between prebuilds.
 */
export async function normalizeNativeProjectsAsync(params: Params): Promise<BackupFileMappings> {
  if (params.platform === 'ios') {
    return await normalizeIosProjectsAsync(params);
  }
  return {};
}

/**
 * Revert the changes made by `normalizeNativeProjectsAsync()`.
 */
export async function revertNormalizeNativeProjectsAsync(
  backupFileMappings: BackupFileMappings
): Promise<void> {
  await Promise.all(
    Object.entries(backupFileMappings).map(([originalPath, backupPath]) =>
      fs.copyFile(backupPath, originalPath)
    )
  );
}

/**
 * `normalizeNativeProjectsAsync()` implementation for iOS.
 */
async function normalizeIosProjectsAsync({
  projectRoot,
  workingDirectories,
  backup,
}: Params): Promise<BackupFileMappings> {
  const backupFileMappings: BackupFileMappings = {};

  // Normalize the pbxproj file since it has some dynamic IDs that change between prebuilds.
  const xcodeProjectPath = IOSConfig.Paths.getPBXProjectPath(projectRoot);
  const xcodeProjectPathBackup = path.join(
    workingDirectories.tmpDir,
    path.basename(xcodeProjectPath)
  );
  if (backup) {
    await fs.copyFile(xcodeProjectPath, xcodeProjectPathBackup);
    backupFileMappings[xcodeProjectPath] = xcodeProjectPathBackup;
  }
  const normalizedProject = await normalizeXcodeProjectAsync(projectRoot, xcodeProjectPath);
  await fs.writeFile(xcodeProjectPath, buildXcodeProject(normalizedProject.toJSON()), 'utf8');

  return backupFileMappings;
}

/**
 * Normalize the Xcode project file by removing some dynamic values.
 * - Remove the `noop-file.swift` file that is generated by the prebuild process.
 * - Remove the Swift bridging header file that is generated by the prebuild process.
 * - Remove the `ExpoModulesProvider.swift` that is generated by expo-modules-autolinking and `pod install`.
 * - Remove the `PrivacyInfo.xcprivacy` file that is generated by `pod install`.
 * - Remove the build phase that is generated by expo-modules-autolinking and `pod install`.
 * - Remove the all the other CocoaPods generated properties.
 */
export async function normalizeXcodeProjectAsync(
  projectRoot: string,
  xcodeProjectPath: string
): Promise<XcodeProject> {
  const projectName = IOSConfig.XcodeUtils.getProjectName(projectRoot);
  const project = XcodeProject.open(xcodeProjectPath);

  // Remove generated PBXGroup
  //   - file references
  //   - generated groups like `Pods` and `Supporting`
  const mainGroup = project.rootObject.props.mainGroup;
  for (const child of mainGroup.props.children) {
    if (child.props.isa === 'PBXGroup' && child.props.name === projectName) {
      const mainAppGroup = child as PBXGroup;

      for (const mainAppGroupChild of mainAppGroup.props.children) {
        if (
          mainAppGroupChild.props.isa === 'PBXFileReference' &&
          ['noop-file.swift', `${projectName}-Bridging-Header.h`, 'PrivacyInfo.xcprivacy'].includes(
            mainAppGroupChild.props.name ?? ''
          )
        ) {
          mainAppGroupChild.removeFromProject();
        }
      }

      for (const mainAppGroupChild of mainAppGroup.getChildGroups()) {
        if (mainAppGroupChild.props.name === 'Supporting') {
          mainAppGroupChild.removeFromProject();
        }
      }
    }
    if (child.props.isa === 'PBXGroup' && child.props.name === 'Pods') {
      child.removeFromProject();
    }
  }

  // Remove ExpoModulesProviders group
  for (const child of mainGroup.getChildGroups()) {
    if (child.props.isa === 'PBXGroup' && child.props.name === 'ExpoModulesProviders') {
      for (const grandChild of child.getChildGroups()) {
        grandChild.removeFromProject();
      }
      child.removeFromProject();
    }
  }

  // Remove shell script build phases
  const mainAppTarget = project.rootObject.getMainAppTarget();
  for (const removeBuildPhaseName of [
    '[Expo] Configure project',
    '[CP] Embed Pods Frameworks',
    '[CP] Check Pods Manifest.lock',
    '[CP] Copy Pods Resources',
  ]) {
    const buildPhases = mainAppTarget?.props.buildPhases;
    for (const buildPhase of buildPhases ?? []) {
      if (
        buildPhase?.isa === 'PBXShellScriptBuildPhase' &&
        (buildPhase as PBXShellScriptBuildPhase).props.name === removeBuildPhaseName
      ) {
        buildPhase.removeFromProject();
        removeChildrenByUuid(buildPhases ?? [], buildPhase.uuid);
      }
    }
  }

  // Remove frameworks build phase
  const frameworksBuildPhase = mainAppTarget?.getFrameworksBuildPhase();
  for (const file of frameworksBuildPhase?.props.files ?? []) {
    if (
      file.isa === 'PBXBuildFile' &&
      file.props.fileRef.props.path === `libPods-${projectName}.a`
    ) {
      removeChildrenByUuid(frameworksBuildPhase?.props.files ?? [], file.uuid);
    }
  }
  // Remove resources build phase
  const resourcesBuildPhase = mainAppTarget?.getResourcesBuildPhase();
  for (const file of resourcesBuildPhase?.props.files ?? []) {
    if (file.isa === 'PBXBuildFile' && file.props.fileRef.props.name === 'PrivacyInfo.xcprivacy') {
      removeChildrenByUuid(resourcesBuildPhase?.props.files ?? [], file.uuid);
    }
  }
  const sourcesBuildPhase = mainAppTarget?.getSourcesBuildPhase();
  for (const file of sourcesBuildPhase?.props.files ?? []) {
    if (
      file.isa === 'PBXBuildFile' &&
      ['ExpoModulesProvider.swift', 'PrivacyInfo.xcprivacy'].includes(
        file.props.fileRef.props.name ?? ''
      )
    ) {
      removeChildrenByUuid(sourcesBuildPhase?.props.files ?? [], file.uuid);
    }
  }

  // Remove frameworks group
  const frameworksGroup = project.rootObject.getFrameworksGroup();
  for (const child of frameworksGroup.props.children) {
    if (child.props.isa === 'PBXFileReference' && child.props.path === `libPods-${projectName}.a`) {
      child.removeFromProject();
      removeChildrenByUuid(frameworksGroup.props.children ?? [], child.uuid);
    }
  }

  // Remove `baseConfigurationReference` for Pods xcconfig
  const buildConfigurationList = mainAppTarget?.props.buildConfigurationList;
  for (const buildConfiguration of buildConfigurationList?.props.buildConfigurations ?? []) {
    if (
      [`Pods-${projectName}.debug.xcconfig`, `Pods-${projectName}.release.xcconfig`].includes(
        buildConfiguration.props.baseConfigurationReference?.props.name ?? ''
      )
    ) {
      delete buildConfiguration.props.baseConfigurationReference;
    }
  }

  // Remove additional file references
  for (const child of project.values()) {
    if (
      child.isa === 'PBXFileReference' &&
      [
        'ExpoModulesProvider.swift',
        `Pods-${projectName}.debug.xcconfig`,
        `Pods-${projectName}.release.xcconfig`,
      ].includes((child as PBXFileReference).props.name ?? '')
    ) {
      child.removeFromProject();
    }
  }

  return project;
}

/**
 * Remove a child by its UUID from a pbx array.
 */
function removeChildrenByUuid<T extends { uuid: string }>(children: T[], uuid: string) {
  const index = children.findIndex((child) => child.uuid === uuid);
  if (index >= 0) {
    children?.splice(index, 1);
  }
}
